import { FormsModule } from '@angular/forms';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, TemplateRef, ViewChild } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { DatePickerModule } from './date-picker.module';
import { OverlayService } from '../core/api';
import { LvDateTimeOption } from './interface';

const date = new Date('2023-07-31');

const ranges = [{ 'Next 7 days': [date, new Date(date.getTime() + 3600 * 1000 * 24 * 7)] }];

@Component({
  template: `
    <lv-date-range-picker
      [(ngModel)]="date"
      [lvSize]="lvSize"
      [lvFormat]="lvFormat"
      [lvInline]="lvInline"
      [lvReadonly]="lvReadonly"
      [lvDisabled]="lvDisabled"
      [lvShowClear]="lvShowClear"
      [lvPlaceholder]="lvPlaceholder"
      [lvPanelClass]="lvPanelClass"
      [lvShowTime]="lvShowTime"
      [lvNowAsBaseTime]="lvNowAsBaseTime"
      [lvDisabledDate]="lvDisabledDate"
      [lvOnlyShowActiveCell]="lvOnlyShowActiveCell"
      [lvPanelTitleReadonly]="lvPanelTitleReadonly"
      [lvCalendarMonthOperate]="lvCalendarMonthOperate"
      [lvCalendarYearOperate]="lvCalendarYearOperate"
      [lvTimezoneOffset]="lvTimezoneOffset"
      [lvFooterExtra]="lvFooterExtra"
      [lvPresetRanges]="lvPresetRanges"
      (ngModelChange)="modelChange($event)"
      (lvOpenChange)="openChange()"
      (lvActiveDateChange)="activeDateChange()"
      (lvRangeTagChange)="rangeTagChange()"
    ></lv-date-range-picker>

    <ng-template #footerTpl>
      <div class="lv-date-picker-cell" style="padding: 8px 24px; background: #fafafa; width: 100%">Here is the expanded content!</div>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestDateRangePickerComponent {
  @ViewChild('footerTpl', { static: true }) footerTpl: TemplateRef<Element>;

  date;
  lvSize: 'default' | 'large' = 'default';
  lvFormat = 'yyyy-MM-dd HH:mm:ss';
  lvPlaceholder = ['开始日期', '结束日期'];
  lvInline = false;
  lvReadonly = false;
  lvDisabled = false;
  lvShowClear = true;
  lvPanelClass = '';
  lvShowTime: boolean | LvDateTimeOption = false;
  lvNowAsBaseTime = true;
  lvDisabledDate: (date: Date) => boolean;
  lvOnlyShowActiveCell = false;
  lvPanelTitleReadonly = false;
  lvCalendarMonthOperate = { show: true, disableChange: false };
  lvCalendarYearOperate = { show: true, disableChange: false };
  lvTimezoneOffset;
  lvPresetRanges;
  lvFooterExtra;

  openChange() {}

  activeDateChange() {}

  modelChange(e) {}

  rangeTagChange() {}
}

describe('DateRangePickerComponent', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [TestDateRangePickerComponent],
      imports: [DatePickerModule, FormsModule, NoopAnimationsModule],
      teardown: { destroyAfterEach: true },
      providers: [OverlayService],
    }).compileComponents();
  }));

  describe('render', () => {
    let component: TestDateRangePickerComponent;
    let fixture: ComponentFixture<TestDateRangePickerComponent>;

    const delay = () => {
      tick(500);
      fixture.detectChanges();
    };

    const renderComponent = (param, genSnapshot = true) => {
      for (const [key, value] of Object.entries(param)) {
        component[key] = value;
      }
      delay();
      delay();
      if (genSnapshot) {
        expect(fixture).toMatchSnapshot();
      }
    };

    const getPanel = () => {
      const button = fixture.nativeElement.querySelector('.lv-date-picker-operation');
      button.dispatchEvent(new Event('click'));
      delay();
      delay();
      return document.querySelector('lv-date-picker-panel');
    };

    beforeEach(() => {
      fixture = TestBed.createComponent(TestDateRangePickerComponent);
      component = fixture.componentInstance;
      component.date = [new Date('2023-06-10'), new Date('2023-07-01')];
    });

    it('with basic', fakeAsync(() => {
      renderComponent({});
      expect(getPanel()).toMatchSnapshot();
    }));

    it('with show time', fakeAsync(() => {
      renderComponent({ lvShowTime: true });
      expect(getPanel()).toMatchSnapshot();
    }));

    it('with large size', fakeAsync(() => {
      renderComponent({ lvSize: 'large' });
    }));

    it('with disabled', fakeAsync(() => {
      renderComponent({ lvDisabled: true });
    }));

    it('with format', fakeAsync(() => {
      renderComponent({ lvFormat: 'yyyy/MM/dd' });
    }));

    it('with disabled date', fakeAsync(() => {
      renderComponent(
        {
          date: [new Date('2023-06-10 08:59:59'), new Date('2023-07-01 08:59:59')],
          lvDisabledDate: date => {
            if (date > component.date) {
              return true;
            }
          },
          lvShowTime: {
            format: 'HH:mm:ss',
            disabledHour: hour => {
              if (hour < 5 || hour > 18) {
                return true;
              }
            },
            disabledMinute: (hour, minute) => {
              if (hour == 8 && minute < 30) {
                return true;
              }
            },
            disabledSecond: (hour, minute, second) => {
              if (minute < 30 && second > 30) {
                return true;
              }
            },
          },
        },
        false
      );
      expect(getPanel()).toMatchSnapshot();
    }));

    it('with preset ranges', fakeAsync(() => {
      renderComponent({ lvPresetRanges: ranges }, false);
      expect(getPanel()).toMatchSnapshot();
    }));

    it('with footer extra', fakeAsync(() => {
      renderComponent({ lvFooterExtra: component.footerTpl }, false);
      expect(getPanel()).toMatchSnapshot();
    }));
  });

  describe('work', () => {
    let component: TestDateRangePickerComponent;
    let fixture: ComponentFixture<TestDateRangePickerComponent>;
    let cdr: ChangeDetectorRef;

    const mouseEvent = (el: Element, event = 'click') => {
      el.dispatchEvent(new Event(event));
    };

    const delay = () => {
      tick(500);
      fixture.detectChanges();
    };

    const renderComponent = param => {
      for (const [key, value] of Object.entries(param)) {
        component[key] = value;
      }
      delay();
      delay();
    };

    const getInput = (type: 'start' | 'end' = 'start') => {
      const [start, end] = fixture.nativeElement.querySelectorAll('.lv-date-picker-main .lv-input');
      return type === 'start' ? start : end;
    };

    const getConfirm = () => document.querySelector('.lv-date-picker-panel-footer .lv-button-type-primary');

    const getPanel = (type: 'start' | 'end' = 'start') => {
      const input = getInput(type);
      mouseEvent(input);
      delay();
      delay();
      const [start, end] = Array.from(document.querySelectorAll('lv-date-picker-panel'));
      return { start, end };
    };

    const getDay = (day: string, type: 'start' | 'end' = 'start') => {
      const { start, end } = getPanel(type);
      const days: Element[] = Array.from(start.querySelectorAll('.lv-date-picker-cell'));
      if (end) {
        const endDays: Element[] = Array.from(end.querySelectorAll('.lv-date-picker-cell'));
        days.push(...endDays);
      }

      return days.filter(item => {
        if (!item.parentElement.className.includes('lv-date-picker-last-month-day')) {
          return item.textContent.trim() === day;
        } else {
          return false;
        }
      });
    };

    beforeEach(() => {
      fixture = TestBed.createComponent(TestDateRangePickerComponent);
      component = fixture.componentInstance;
      cdr = fixture.debugElement.injector.get(ChangeDetectorRef);
      component.date = [new Date('2023-07-10'), new Date('2023-08-01')];
    });

    it('with modelChange', fakeAsync(() => {
      jest.spyOn(component, 'modelChange');
      renderComponent({});

      const startDay = getDay('20')[0];
      mouseEvent(startDay, 'click');
      delay();
      delay();
      expect(getInput('start').value).toContain('2023-07-20');
      expect(component.modelChange).toBeCalledWith([new Date('2023-07-20'), new Date('2023-08-01')]);

      const endDay = getDay('20', 'end')[1];
      mouseEvent(endDay, 'click');
      delay();
      delay();
      expect(getInput('end').value).toContain('2023-08-20');
      expect(component.modelChange).toBeCalledWith([new Date('2023-07-20'), new Date('2023-08-20')]);
    }));

    it('with click start more than end time', fakeAsync(() => {
      jest.spyOn(component, 'modelChange');
      renderComponent({});

      const startDay = getDay('30', 'start')[1];
      mouseEvent(startDay, 'click');
      delay();
      delay();
      expect(getInput('start').value).toContain('2023-07-10');
      expect(component.modelChange).not.toBeCalled();
    }));

    it('with click end less than start time', fakeAsync(() => {
      jest.spyOn(component, 'modelChange');
      renderComponent({});

      const endDay = getDay('1', 'end')[0];
      mouseEvent(endDay, 'click');
      delay();
      delay();
      expect(getInput('end').value).toContain('2023-08-01');
      expect(component.modelChange).not.toBeCalled();
    }));

    it('with value change', fakeAsync(() => {
      renderComponent({});

      expect(getDay('10')[0].parentElement.className).toContain('lv-date-picker-selected-day');
      expect(getDay('1', 'end')[1].parentElement.className).toContain('lv-date-picker-selected-day');

      const toggleButton = fixture.nativeElement.querySelector('.lv-date-picker-operation');
      mouseEvent(toggleButton, 'click');
      delay();
      delay();
      const newDate = [new Date('2023-07-20'), new Date('2023-08-10')];
      component.date = newDate;
      cdr.detectChanges();
      delay();
      expect(getInput().value).toContain('2023-07-20');
      expect(getDay('20')[0].parentElement.className).toContain('lv-date-picker-selected-day');
      expect(getDay('10', 'end')[1].parentElement.className).toContain('lv-date-picker-selected-day');
    }));

    it('with preset range', fakeAsync(() => {
      renderComponent({ lvPresetRanges: ranges });

      const buttons = getPanel().start.querySelectorAll('.lv-date-picker-panel-presets-item');
      expect(buttons[0].innerHTML).toContain('Next 7 days');
      mouseEvent(buttons[0], 'click');
      delay();
      delay();
      expect(getInput().value).toContain('2023-07-31');
      expect(getInput('end').value).toContain('2023-08-07');
    }));

    it('with rangeTag change', fakeAsync(() => {
      jest.spyOn(component, 'rangeTagChange');
      renderComponent({});
      getPanel();
      expect(component.rangeTagChange).toHaveBeenCalledTimes(1);
      getPanel('end');
      expect(component.rangeTagChange).toHaveBeenCalledTimes(2);
    }));

    it('with clear', fakeAsync(() => {
      renderComponent({});
      delay();
      delay();
      expect(getInput().value).toContain('2023-07-10');
      expect(getInput('end').value).toContain('2023-08-01');
      const clearIcon = fixture.nativeElement.querySelector('.lv-date-picker-clear');
      clearIcon.dispatchEvent(new Event('click'));
      delay();
      delay();
      expect(getInput().value).toBeFalsy();
      expect(getInput('end').value).toBeFalsy();
    }));

    it('with visible change', fakeAsync(() => {
      jest.spyOn(component, 'openChange');
      renderComponent({ lvShowTime: true });
      getPanel();
      expect(component.openChange).toBeCalledTimes(1);

      mouseEvent(getConfirm(), 'click');
      delay();
      delay();
      expect(component.openChange).toBeCalledTimes(2);
      expect(document.querySelector('lv-date-picker-panel')).toBeNull();
    }));

    it('with showTime on ok', fakeAsync(() => {
      renderComponent({ lvShowTime: true });
      expect(getInput().value).toContain('2023-07-10 08:00:00');
      expect(getInput('end').value).toContain('2023-08-01 08:00:00');
      getPanel();
      const hours = () => document.querySelectorAll('.lv-time-picker-panel-hour li');
      mouseEvent(hours()[0], 'click');
      delay();
      delay();
      getPanel('end');
      mouseEvent(hours()[0], 'click');
      delay();
      delay();
      mouseEvent(getConfirm(), 'click');
      delay();
      delay();
      expect(getInput().value).toContain('2023-07-10 00:00:00');
      expect(getInput('end').value).toContain('2023-08-01 00:00:00');
    }));
  });
});
